import pandas as pd
import numpy as np
import plotly.express as px ## 코로플레스를 만들 수 있게 해준다.
import json
import requestsPlotly | 월드맵 시각화(1)
이번엔
plotly로 그래픽스를 만들거다… ~(용량이 더럽게 커서 깃허브가 하지 말랜다)~
1. 라이브러리 imports
2. 에너지사용량 시각화
전국 에너지 사용량 정보와 코로플레스를 이용하여 시각화를 해보자.
A. 데이터 불러오기
global_dict = json.loads(requests.get('https://raw.githubusercontent.com/southkorea/southkorea-maps/master/kostat/2018/json/skorea-provinces-2018-geo.json').text)
local_dict = json.loads(requests.get('https://raw.githubusercontent.com/southkorea/southkorea-maps/master/kostat/2018/json/skorea-municipalities-2018-geo.json').text)
#--#
url = 'https://raw.githubusercontent.com/guebin/DV2022/main/posts/Energy/{}.csv'
prov = ['Seoul', 'Busan', 'Daegu', 'Incheon',
'Gwangju', 'Daejeon', 'Ulsan', 'Sejongsi',
'Gyeonggi-do', 'Gangwon-do', 'Chungcheongbuk-do',
'Chungcheongnam-do', 'Jeollabuk-do', 'Jeollanam-do',
'Gyeongsangbuk-do', 'Gyeongsangnam-do', 'Jeju-do']
df = pd.concat([pd.read_csv(url.format(p+y)).assign(년도=y, 시도=p) for p in prov for y in ['2018', '2019', '2020', '2021']]).reset_index(drop=True)\
.assign(년도 = lambda df: df.년도.astype(int))\
.set_index(['년도','시도','지역']).applymap(lambda x: int(str(x).replace(',','')))\
.reset_index()
df.head()/tmp/ipykernel_235844/2433387610.py:12: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead.
.set_index(['년도','시도','지역']).applymap(lambda x: int(str(x).replace(',','')))\
| 년도 | 시도 | 지역 | 건물동수 | 연면적 | 에너지사용량(TOE)/전기 | 에너지사용량(TOE)/도시가스 | 에너지사용량(TOE)/지역난방 | |
|---|---|---|---|---|---|---|---|---|
| 0 | 2018 | Seoul | 종로구 | 17929 | 9141777 | 64818 | 82015 | 111 |
| 1 | 2018 | Seoul | 중구 | 10598 | 10056233 | 81672 | 75260 | 563 |
| 2 | 2018 | Seoul | 용산구 | 17201 | 10639652 | 52659 | 85220 | 12043 |
| 3 | 2018 | Seoul | 성동구 | 14180 | 11631770 | 60559 | 107416 | 0 |
| 4 | 2018 | Seoul | 광진구 | 21520 | 12054796 | 70609 | 130308 | 0 |
지역 좌표 정보를 가진 데이터와, 지역의 에너지 사용량 관련 정보를 가진 데이터를 불러왔다.
### B. 데이터 정리(한글로 변환)
df의 영어로 된 시도를 global_dict를 이용하여 한글로 바꿔주자.
(1) global_dict 내의 영어이름과 df의 영어이름이 일치하는지 확인
set(df.시도) == {l['properties']['name_eng'] for l in global_dict['features']} ## global_dict는 커다란 딕셔너리에 features가 있고, 그곳에 지역들의 리스트가 있다.True
{l['properties']['name_eng'] for l in global_dict['features']}{'Busan',
'Chungcheongbuk-do',
'Chungcheongnam-do',
'Daegu',
'Daejeon',
'Gangwon-do',
'Gwangju',
'Gyeonggi-do',
'Gyeongsangbuk-do',
'Gyeongsangnam-do',
'Incheon',
'Jeju-do',
'Jeollabuk-do',
'Jeollanam-do',
'Sejongsi',
'Seoul',
'Ulsan'}
딕셔너리의
key만으로 딕셔너리 컴프리헨션 하면set()을 한 것처럼 알파벳 순서대로key값만 나온다.(아니면 그냥 리스트 컴프리헨션 하고set()걸어주던지…)
(2) global_dict 내의 영어이름과 한글이름을 이용해 변환을 위한 dictionary 생성
_dct = {l['properties']['name_eng'] : l['properties']['name'] for l in global_dict['features']}
_dct{'Seoul': '서울특별시',
'Busan': '부산광역시',
'Daegu': '대구광역시',
'Incheon': '인천광역시',
'Gwangju': '광주광역시',
'Daejeon': '대전광역시',
'Ulsan': '울산광역시',
'Sejongsi': '세종특별자치시',
'Gyeonggi-do': '경기도',
'Gangwon-do': '강원도',
'Chungcheongbuk-do': '충청북도',
'Chungcheongnam-do': '충청남도',
'Jeollabuk-do': '전라북도',
'Jeollanam-do': '전라남도',
'Gyeongsangbuk-do': '경상북도',
'Gyeongsangnam-do': '경상남도',
'Jeju-do': '제주특별자치도'}
(3) df에 변환을 수행하여 영어지명을 한글지명으로 변환
df.assign(시도 = lambda _df : _df.시도.apply(lambda x : [v for k, v in _dct.items() if x == k].pop()))| 년도 | 시도 | 지역 | 건물동수 | 연면적 | 에너지사용량(TOE)/전기 | 에너지사용량(TOE)/도시가스 | 에너지사용량(TOE)/지역난방 | |
|---|---|---|---|---|---|---|---|---|
| 0 | 2018 | 서울특별시 | 종로구 | 17929 | 9141777 | 64818 | 82015 | 111 |
| 1 | 2018 | 서울특별시 | 중구 | 10598 | 10056233 | 81672 | 75260 | 563 |
| 2 | 2018 | 서울특별시 | 용산구 | 17201 | 10639652 | 52659 | 85220 | 12043 |
| 3 | 2018 | 서울특별시 | 성동구 | 14180 | 11631770 | 60559 | 107416 | 0 |
| 4 | 2018 | 서울특별시 | 광진구 | 21520 | 12054796 | 70609 | 130308 | 0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 995 | 2019 | 제주특별자치도 | 서귀포시 | 34729 | 7233931 | 34641 | 1306 | 0 |
| 996 | 2020 | 제주특별자치도 | 제주시 | 66504 | 19819923 | 99212 | 22179 | 0 |
| 997 | 2020 | 제주특별자치도 | 서귀포시 | 34880 | 7330040 | 35510 | 1639 | 0 |
| 998 | 2021 | 제주특별자치도 | 제주시 | 67053 | 20275738 | 103217 | 25689 | 0 |
| 999 | 2021 | 제주특별자치도 | 서귀포시 | 35230 | 7512206 | 37884 | 2641 | 0 |
1000 rows × 8 columns
df.시도.map(_dct) ## 람다 이것저것 안쓰고 이렇게 하면 한번에 할 수도 있다.0 서울특별시
1 서울특별시
2 서울특별시
3 서울특별시
4 서울특별시
...
995 제주특별자치도
996 제주특별자치도
997 제주특별자치도
998 제주특별자치도
999 제주특별자치도
Name: 시도, Length: 1000, dtype: object
(4) local_dict와 global_dict의 지명정보를 정리하여 데이터프레임으로 만듦
# 예비학습
pd.DataFrame(
[{'X':100,'y':0},
{'X':101,'y':1}]
) ## 딕셔너리를 리스트로 넣어버리면 행이 분리된 채로 넣어줄수도 있다.| X | y | |
|---|---|---|
| 0 | 100 | 0 |
| 1 | 101 | 1 |
위와 동일한 원리로 코드와 이름에 해당하는 애들을 추출함(
'properties'에는 지역에 대한 코드 정보가 딕셔너리로 포함되어 있음)
pd.DataFrame([l['properties'] for l in local_dict['features']])| name | base_year | name_eng | code | |
|---|---|---|---|---|
| 0 | 종로구 | 2018 | Jongno-gu | 11010 |
| 1 | 중구 | 2018 | Jung-gu | 11020 |
| 2 | 용산구 | 2018 | Yongsan-gu | 11030 |
| 3 | 성동구 | 2018 | Seongdong-gu | 11040 |
| 4 | 광진구 | 2018 | Gwangjin-gu | 11050 |
| ... | ... | ... | ... | ... |
| 245 | 함양군 | 2018 | Hamyang-gun | 38380 |
| 246 | 거창군 | 2018 | Geochang-gun | 38390 |
| 247 | 합천군 | 2018 | Hapcheon-gun | 38400 |
| 248 | 제주시 | 2018 | Jeju-si | 39010 |
| 249 | 서귀포시 | 2018 | Seogwipo-si | 39020 |
250 rows × 4 columns
df_local = pd.DataFrame([l['properties'] for l in local_dict['features']])\
.drop(['name_eng','base_year'],axis=1)
df_local| name | code | |
|---|---|---|
| 0 | 종로구 | 11010 |
| 1 | 중구 | 11020 |
| 2 | 용산구 | 11030 |
| 3 | 성동구 | 11040 |
| 4 | 광진구 | 11050 |
| ... | ... | ... |
| 245 | 함양군 | 38380 |
| 246 | 거창군 | 38390 |
| 247 | 합천군 | 38400 |
| 248 | 제주시 | 39010 |
| 249 | 서귀포시 | 39020 |
250 rows × 2 columns
df_global = pd.DataFrame([l['properties'] for l in global_dict['features']])\
.drop(['name_eng','base_year'],axis=1)
df_global| name | code | |
|---|---|---|
| 0 | 서울특별시 | 11 |
| 1 | 부산광역시 | 21 |
| 2 | 대구광역시 | 22 |
| 3 | 인천광역시 | 23 |
| 4 | 광주광역시 | 24 |
| 5 | 대전광역시 | 25 |
| 6 | 울산광역시 | 26 |
| 7 | 세종특별자치시 | 29 |
| 8 | 경기도 | 31 |
| 9 | 강원도 | 32 |
| 10 | 충청북도 | 33 |
| 11 | 충청남도 | 34 |
| 12 | 전라북도 | 35 |
| 13 | 전라남도 | 36 |
| 14 | 경상북도 | 37 |
| 15 | 경상남도 | 38 |
| 16 | 제주특별자치도 | 39 |
(5) df_local에서 “전주시완산구”와 같이 정리된 지명들을 “완산구”로 변환
df_local.name0 종로구
1 중구
2 용산구
3 성동구
4 광진구
...
245 함양군
246 거창군
247 합천군
248 제주시
249 서귀포시
Name: name, Length: 250, dtype: object
_dct = {n : n.split('시')[-1] for n in df_local.name if '시' in n and len(n) > 3 and n[-1] == '구'}
_dct{'수원시장안구': '장안구',
'수원시권선구': '권선구',
'수원시팔달구': '팔달구',
'수원시영통구': '영통구',
'성남시수정구': '수정구',
'성남시중원구': '중원구',
'성남시분당구': '분당구',
'안양시만안구': '만안구',
'안양시동안구': '동안구',
'안산시상록구': '상록구',
'안산시단원구': '단원구',
'고양시덕양구': '덕양구',
'고양시일산동구': '일산동구',
'고양시일산서구': '일산서구',
'용인시처인구': '처인구',
'용인시기흥구': '기흥구',
'용인시수지구': '수지구',
'청주시상당구': '상당구',
'청주시서원구': '서원구',
'청주시흥덕구': '흥덕구',
'청주시청원구': '청원구',
'천안시동남구': '동남구',
'천안시서북구': '서북구',
'전주시완산구': '완산구',
'전주시덕진구': '덕진구',
'포항시남구': '남구',
'포항시북구': '북구',
'창원시의창구': '의창구',
'창원시성산구': '성산구',
'창원시마산합포구': '마산합포구',
'창원시마산회원구': '마산회원구',
'창원시진해구': '진해구'}
'시'가 들어간 이름들을 변환
df_local.set_index('name').rename(_dct).reset_index() ## 인덱스 지정과 rename을 이용하여 딕셔너리를 활용함!!| name | code | |
|---|---|---|
| 0 | 종로구 | 11010 |
| 1 | 중구 | 11020 |
| 2 | 용산구 | 11030 |
| 3 | 성동구 | 11040 |
| 4 | 광진구 | 11050 |
| ... | ... | ... |
| 245 | 함양군 | 38380 |
| 246 | 거창군 | 38390 |
| 247 | 합천군 | 38400 |
| 248 | 제주시 | 39010 |
| 249 | 서귀포시 | 39020 |
250 rows × 2 columns
이제 시도라는 중복정보를 뺀 데이터프레임이 마련되었다.
(6) df_local과 df_global의 정보를 정리하여 merge, 합쳐진 정보를 df_json에 저장
df_local.set_index('name').rename(_dct).reset_index()\
.rename({'code':'code_local','name':'name_local'},axis=1)\
.assign(code = lambda df: df.code_local.str[:2])
## 코드의 앞 두자리를 공통분모로 삼을 것이다. df_global에는 code 앞 두자리를 넣어놨음| name_local | code_local | code | |
|---|---|---|---|
| 0 | 종로구 | 11010 | 11 |
| 1 | 중구 | 11020 | 11 |
| 2 | 용산구 | 11030 | 11 |
| 3 | 성동구 | 11040 | 11 |
| 4 | 광진구 | 11050 | 11 |
| ... | ... | ... | ... |
| 245 | 함양군 | 38380 | 38 |
| 246 | 거창군 | 38390 | 38 |
| 247 | 합천군 | 38400 | 38 |
| 248 | 제주시 | 39010 | 39 |
| 249 | 서귀포시 | 39020 | 39 |
250 rows × 3 columns
df_json = df_local.set_index('name').rename(_dct).reset_index()\
.rename({'code':'code_local','name':'name_local'},axis=1)\
.assign(code = lambda df: df.code_local.str[:2])\
.merge(df_global) ## 공통열이 `code`밖에 없으므로, 알아서 엮어준다.
df_json| name_local | code_local | code | name | |
|---|---|---|---|---|
| 0 | 종로구 | 11010 | 11 | 서울특별시 |
| 1 | 중구 | 11020 | 11 | 서울특별시 |
| 2 | 용산구 | 11030 | 11 | 서울특별시 |
| 3 | 성동구 | 11040 | 11 | 서울특별시 |
| 4 | 광진구 | 11050 | 11 | 서울특별시 |
| ... | ... | ... | ... | ... |
| 245 | 함양군 | 38380 | 38 | 경상남도 |
| 246 | 거창군 | 38390 | 38 | 경상남도 |
| 247 | 합천군 | 38400 | 38 | 경상남도 |
| 248 | 제주시 | 39010 | 39 | 제주특별자치도 |
| 249 | 서귀포시 | 39020 | 39 | 제주특별자치도 |
250 rows × 4 columns
(7) df_json과 df의 정보를 merge하기 위하여 ’서울특별시-종로구’와 같은 형식으로 공통열을 각각 생성, 생성된 공통열의 원소가 일치하는지 비교
s1 = df_json.assign(on = lambda df: df.name + '-' + df.name_local)['on']
s10 서울특별시-종로구
1 서울특별시-중구
2 서울특별시-용산구
3 서울특별시-성동구
4 서울특별시-광진구
...
245 경상남도-함양군
246 경상남도-거창군
247 경상남도-합천군
248 제주특별자치도-제주시
249 제주특별자치도-서귀포시
Name: on, Length: 250, dtype: object
s2 = df.assign(
시도 = df.시도.map({l['properties']['name_eng']:l['properties']['name'] for l in global_dict['features']})
).assign(on = lambda df: df.시도 + '-' + df.지역)['on']
s20 서울특별시-종로구
1 서울특별시-중구
2 서울특별시-용산구
3 서울특별시-성동구
4 서울특별시-광진구
...
995 제주특별자치도-서귀포시
996 제주특별자치도-제주시
997 제주특별자치도-서귀포시
998 제주특별자치도-제주시
999 제주특별자치도-서귀포시
Name: on, Length: 1000, dtype: object
(싱글벙글)당연히 똑같겠지?
set(s1)-set(s2), set(s2)-set(s1)({'인천광역시-남구'}, {'인천광역시-미추홀구'})
똑같은 방식이었을 텐데 다른 값이 있다.(
df_json에는 남구라고 되어있고,df에는 미추홀구라고 되어있다.)
지역 명이 미추홀구로 바뀌었음!!!
(8) 지역명을 적절히 변환(df_json을 바꿈)
df_json| name_local | code_local | code | name | |
|---|---|---|---|---|
| 0 | 종로구 | 11010 | 11 | 서울특별시 |
| 1 | 중구 | 11020 | 11 | 서울특별시 |
| 2 | 용산구 | 11030 | 11 | 서울특별시 |
| 3 | 성동구 | 11040 | 11 | 서울특별시 |
| 4 | 광진구 | 11050 | 11 | 서울특별시 |
| ... | ... | ... | ... | ... |
| 245 | 함양군 | 38380 | 38 | 경상남도 |
| 246 | 거창군 | 38390 | 38 | 경상남도 |
| 247 | 합천군 | 38400 | 38 | 경상남도 |
| 248 | 제주시 | 39010 | 39 | 제주특별자치도 |
| 249 | 서귀포시 | 39020 | 39 | 제주특별자치도 |
250 rows × 4 columns
df_json.assign(on = lambda _df : _df.name + '-' + _df.name_local).drop(['name', 'name_local'], axis = 1)\
.set_index('on').rename({'인천광역시-남구':'인천광역시-미추홀구'}).reset_index() ## 합치고 없애지 않으면, 모든 남구를 다 없애버린다.| on | code_local | code | |
|---|---|---|---|
| 0 | 서울특별시-종로구 | 11010 | 11 |
| 1 | 서울특별시-중구 | 11020 | 11 |
| 2 | 서울특별시-용산구 | 11030 | 11 |
| 3 | 서울특별시-성동구 | 11040 | 11 |
| 4 | 서울특별시-광진구 | 11050 | 11 |
| ... | ... | ... | ... |
| 245 | 경상남도-함양군 | 38380 | 38 |
| 246 | 경상남도-거창군 | 38390 | 38 |
| 247 | 경상남도-합천군 | 38400 | 38 |
| 248 | 제주특별자치도-제주시 | 39010 | 39 |
| 249 | 제주특별자치도-서귀포시 | 39020 | 39 |
250 rows × 3 columns
(9) 데이터프레임을 결합
df2 = df_json.assign(on = lambda _df : _df.name + '-' + _df.name_local).drop(['name', 'name_local'], axis = 1)\
.set_index('on').rename({'인천광역시-남구':'인천광역시-미추홀구'}).reset_index()\
.merge(df.assign(시도 = df.시도.map({l['properties']['name_eng']:l['properties']['name'] for l in global_dict['features']})).assign(on = lambda df: df.시도 + '-' + df.지역))
df2.drop('on', axis = 1) ## 'on' 열은 단순히 `merge`를 위해 만들어둔 더미 열이므로 없애줌| code_local | code | 년도 | 시도 | 지역 | 건물동수 | 연면적 | 에너지사용량(TOE)/전기 | 에너지사용량(TOE)/도시가스 | 에너지사용량(TOE)/지역난방 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 11010 | 11 | 2018 | 서울특별시 | 종로구 | 17929 | 9141777 | 64818 | 82015 | 111 |
| 1 | 11010 | 11 | 2019 | 서울특별시 | 종로구 | 17851 | 9204140 | 63492 | 76653 | 799 |
| 2 | 11010 | 11 | 2020 | 서울특별시 | 종로구 | 17638 | 9148895 | 60123 | 71263 | 912 |
| 3 | 11010 | 11 | 2021 | 서울특별시 | 종로구 | 22845 | 18551145 | 125179 | 117061 | 0 |
| 4 | 11020 | 11 | 2018 | 서울특별시 | 중구 | 10598 | 10056233 | 81672 | 75260 | 563 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 995 | 39010 | 39 | 2021 | 제주특별자치도 | 제주시 | 67053 | 20275738 | 103217 | 25689 | 0 |
| 996 | 39020 | 39 | 2018 | 제주특별자치도 | 서귀포시 | 34154 | 6914685 | 34470 | 1597 | 0 |
| 997 | 39020 | 39 | 2019 | 제주특별자치도 | 서귀포시 | 34729 | 7233931 | 34641 | 1306 | 0 |
| 998 | 39020 | 39 | 2020 | 제주특별자치도 | 서귀포시 | 34880 | 7330040 | 35510 | 1639 | 0 |
| 999 | 39020 | 39 | 2021 | 제주특별자치도 | 서귀포시 | 35230 | 7512206 | 37884 | 2641 | 0 |
1000 rows × 10 columns
이럼 좌표정보가 들어갔다.(끝났어요!)
C. 시각화
그냥 folium 쓰는 것 마냥 사용하면 된다.
# px.choropleth_mapbox(
# data_frame = df2.loc[df2.년도 == 2018],
# geojson = local_dict,
# featureidkey = 'properties.code',
# locations = 'code_local',
# color = '에너지사용량(TOE)/전기',
# hover_data = ['시도','지역'],
# #---#
# mapbox_style = "carto-positron",
# center={"lat": 36, "lon": 127.5}, ## 다른 맵박스랑 다르게 center를 지정하지 않아도 데이터쪽으로 시선을 돌려주진 않네...
# zoom = 6,
# height = 800,
# width = 800
# )용량이 어머가 없어서 생략했다.
### D. 시각화 2 : 서울의 전기 에너지 사용량
애니메이션도 간편하게 만들 수 있다!!
seoul_dict = local_dict.copy()
seoul_dict['features'] = [l for l in local_dict['features'] if l['properties']['code'][:2] == '11'] ## 서울에 해당하는 자료만 뽑음fig = px.choropleth_mapbox(
geojson = seoul_dict,
featureidkey = 'properties.code',
data_frame = df2,
locations = 'code_local',
color='에너지사용량(TOE)/전기',
hover_data=['시도','지역'],
animation_frame='년도', ## 해당 옵션으로 프레임을 구분함
#---#
mapbox_style="carto-positron",
range_color=[0,400000], ## 이건 부수적임
center={"lat": 37.5665, "lon": 126.9780},
zoom=9,
height=500,
width=700,
)
fig.show(config = {'scrollZoom' : False})